﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Linq.Expressions;
using AirLinq.Providers.Sql;
using AirLinq.Meta;
using System.Reflection;

namespace AirLinq.Providers {
	public class QueryConverter {
		private class MemberSelectionKey {
			private readonly SqlSelectCollectionBase _list;
			private readonly MemberInfo _member;
			public MemberSelectionKey(SqlSelectCollectionBase list, MemberInfo member) {
				if (list == null) {
					throw new ArgumentNullException("list");
				}
				if (member == null) {
					throw new ArgumentNullException("member");
				}
				_list = list;
				_member = member;
			}
			public SqlSelectCollectionBase List {
				get {
					return _list;
				}
			}
			public MemberInfo Member {
				get {
					return _member;
				}
			}
			public override bool Equals(object obj) {
				var key = obj as MemberSelectionKey;
				if (key == null) {
					return false;
				}
				return _list==key._list && _member==key._member;
			}
			public override int GetHashCode() {
				return _list.GetHashCode() ^ _member.GetHashCode();
			}

		}
		private class AnonymouseSelectionTuple {
			public NewExpression NewExpression { get; set; }
			public SqlSelectCollection SelectCollection { get; set; }
		}
		private readonly Dictionary<ParameterExpression, SqlNode> _expressions;
		private readonly Dictionary<SqlSource, SqlSelectCollectionBase> _sourceColumns;
		private readonly Dictionary<MemberSelectionKey, SqlExpression> _memberExpressions;

		private int _parameterCount;
		private MetaTypeTemplate _metaType;
		private AnonymouseSelectionTuple _anonymouseTuple;

		public QueryConverter() {
			_expressions = new Dictionary<ParameterExpression, SqlNode>();
			_sourceColumns = new Dictionary<SqlSource, SqlSelectCollectionBase>();
			_memberExpressions = new Dictionary<MemberSelectionKey, SqlExpression>();
		}
		public SqlNode Visit(AirLinqProvider provider, Expression exp) {
			_metaType = provider.MetaType;
			var node = VisitInner(exp);
			return node;
		}
		private SqlNode VisitInner(Expression exp) {
			if (exp==null){
				return null;
			}
			switch (exp.NodeType) {
				case ExpressionType.Add:
				case ExpressionType.AddChecked:
				case ExpressionType.Coalesce:
				case ExpressionType.Divide:
				case ExpressionType.ExclusiveOr:
				case ExpressionType.Modulo:
				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
				case ExpressionType.Power:
				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
					return VisitBinary((BinaryExpression)exp);
				case ExpressionType.And:
				case ExpressionType.AndAlso:
				case ExpressionType.Or:
				case ExpressionType.OrElse:
					return VisitBinaryLogical((BinaryExpression)exp);
				case ExpressionType.Equal:
				case ExpressionType.NotEqual:
				case ExpressionType.GreaterThan:
				case ExpressionType.GreaterThanOrEqual:
				case ExpressionType.LessThan:
				case ExpressionType.LessThanOrEqual:
					return VisitComparision((BinaryExpression) exp);
				case ExpressionType.ArrayLength:
					return VisitArrayLength((BinaryExpression) exp);
				case ExpressionType.ArrayIndex:
					return VisitArrayIndex((BinaryExpression)exp);
				case ExpressionType.Call:
					return VisitMethodCall((MethodCallExpression)exp);
				case ExpressionType.Conditional:
					return VisitConditional((ConditionalExpression)exp);
				case ExpressionType.Constant:
					return VisitConstant((ConstantExpression)exp);
				case ExpressionType.Convert:
				case ExpressionType.ConvertChecked:
					return VisitCast((UnaryExpression)exp);
				case ExpressionType.Invoke:
					return VisitInvocation((InvocationExpression)exp);
				case ExpressionType.Lambda:
					return VisitLambda((LambdaExpression)exp);
				case ExpressionType.LeftShift:
				case ExpressionType.RightShift:
					throw new NotSupportedException("shift not supported");
				case ExpressionType.MemberAccess:
					return VisitMemberAccess((MemberExpression)exp);
				case ExpressionType.MemberInit:
					return VisitMemberInit((MemberInitExpression)exp);
				case ExpressionType.Negate:
				case ExpressionType.NegateChecked:
				case ExpressionType.Not:
				case ExpressionType.UnaryPlus:
					return VisitUnary((UnaryExpression)exp);
				case ExpressionType.New:
					return VisitNew((NewExpression)exp);
				case ExpressionType.NewArrayInit:
					return VisitNewArray((NewArrayExpression)exp);
				case ExpressionType.Parameter:
					return VisitParameter((ParameterExpression)exp);
				case ExpressionType.Quote:
					return this.VisitInner(((UnaryExpression)exp).Operand);
				case ExpressionType.TypeAs:
					return VisitAs((UnaryExpression)exp);
				case ExpressionType.TypeIs:
					return VisitTypeBinary((TypeBinaryExpression)exp);
				
				case ExpressionType.ListInit:
				case ExpressionType.NewArrayBounds:
					throw new NotSupportedException("expression type not supported");
			}
			throw new NotSupportedException("expression type not supported");
		}
		private SqlNode VisitTypeBinary(TypeBinaryExpression typeBinaryExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitAs(UnaryExpression unaryExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitParameter(ParameterExpression parameterExpression) {
			SqlNode reference;
			if (_expressions.TryGetValue(parameterExpression, out reference)){
				return reference;
			}
			throw new NotImplementedException();
		}
		private SqlNode VisitNewArray(NewArrayExpression newArrayExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitNew(NewExpression newExpression) {
			//throw new NotImplementedException();
			// TODO Make it only for autogenerated class
			var items = new List<SelectItem>(newExpression.Arguments.Count);
			for(int i=0; i< newExpression.Arguments.Count; i++){
				items.Add(new SelectItem(VisitInner(newExpression.Arguments[i]) as SqlExpression, newExpression.Constructor.GetParameters()[i].Name));
			}
			var res = new SqlSelectCollection(items.AsReadOnly());
			_anonymouseTuple = new AnonymouseSelectionTuple {
				NewExpression=newExpression,
				SelectCollection=res
			};
			return res;
		}
		private SqlNode VisitUnary(UnaryExpression unaryExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitMemberInit(MemberInitExpression memberInitExpression) {
			throw new NotImplementedException();
		}
		private SqlExpression VisitMemberAccess(MemberExpression memberExpression) {
			var expression = VisitInner(memberExpression.Expression);
			var selectionList = expression as SqlSelectCollection;
			if (selectionList != null) {
				SqlExpression expr;
				if (!_memberExpressions.TryGetValue(new MemberSelectionKey(selectionList,memberExpression.Member), out expr)){
					throw new NotSupportedException();
				}
				return expr;
			}
			return null;
		}
		private SqlNode VisitLambda(LambdaExpression lambdaExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitInvocation(InvocationExpression invocationExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitCast(UnaryExpression unaryExpression) {
			return VisitInner(unaryExpression.Operand);
		}
		private SqlNode VisitConstant(ConstantExpression constantExpression) {
			return new SqlParameter(string.Format("P_{0}",_parameterCount++),constantExpression.Value);
		}
		private SqlNode VisitConditional(ConditionalExpression conditionalExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitArrayIndex(BinaryExpression binaryExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitArrayLength(BinaryExpression binaryExpression) {
			throw new NotImplementedException();
		}
		private SqlNode VisitMethodCall(MethodCallExpression exp) {
			if (exp.Method.IsStatic) {
				return VisitStaticMethodCall(exp);
			} else {
				throw new NotImplementedException("method call not supported");
			}
		}
		private SqlNode VisitStaticMethodCall(MethodCallExpression exp) {
			if (exp.Method.GetGenericMethodDefinition() == QueryableMethods.QSelect1) {
				return VisitSelect(exp.Arguments[0], GetLambda(exp.Arguments[1]));
			}
			if (exp.Method.GetGenericMethodDefinition() == QueryableMethods.PFromTable) {
				return VisitFrom(exp.Arguments[0]);
			}
			throw new NotImplementedException("methods not implemented yet");
		}

		private SqlTable VisitFrom(Expression expression) {
			var param=expression as ConstantExpression;
			if (param==null || param.Type != typeof(string)){
				throw new NotImplementedException();
			}
			var source = new SqlTable((string)param.Value);
			var tableRef = new SelectRef(source);
			var selectionList=GetSelectionList(tableRef);
			_sourceColumns[source] = selectionList;
			return source;
		}

		private SqlQueryExpressionBase VisitSelect(Expression query, LambdaExpression selector) {
			var source=VisitSource(query);
			SqlSelectCollectionBase sourceSelectionList;
			if (!_sourceColumns.TryGetValue(source, out sourceSelectionList)) {
				throw new NotSupportedException("selection list for source not found");
			}
			_expressions[selector.Parameters[0]] = sourceSelectionList;
			var selectionList = VisitSelectionList(selector.Body);
			return new SqlQuerySpecification(selectionList, source, null, null);
		}
		private SqlSelectCollectionBase GetSelectionList(SelectRef tableRef) {
			var list = new List<SelectItem>();
			var memberExpressions = new Dictionary<MemberInfo, SqlExpression>();
			foreach (var member in _metaType.Members) {
				if (!string.IsNullOrEmpty(member.ColumnName)) {
					var column=new SqlColumn(member.ColumnName, tableRef);
					//TODO Нужно еще задавать корректный Alias для столбца
					list.Add(new SelectItem(column, null));
					memberExpressions.Add(member.Member, column);
				} else if (member.Expression != null) {
					var sqlNode = VisitInner(member.Expression);
					var sqlExpr = sqlNode as SqlExpression;
					if (sqlExpr != null) {
						//TODO Нужно еще задавать корректный Alias для выражения
						list.Add(new SelectItem(sqlExpr,null));
						memberExpressions.Add(member.Member, sqlExpr);
					} else {
						throw new NotImplementedException();
					}
				}
			}
			var res = new SqlSelectCollection(list.AsReadOnly());
			foreach (var item in memberExpressions) {
				_memberExpressions.Add(new MemberSelectionKey(res, item.Key), item.Value);
			}
			return res;
		}
		private SqlSelectCollectionBase VisitSelectionList(Expression exp) {
			var node = VisitInner(exp);
			var selectionList=node as SqlSelectCollectionBase;
			if (selectionList!=null){
				return selectionList;			
			}
			var expr = node as SqlExpression;
			if (expr != null) {
				//TODO тут нужно сменить _metaType
				//TODO тут нужно задать корректный alias
				var selectionItem = new SelectItem(expr, null);
				var list=new List<SelectItem>{selectionItem};
				//TODO тут нужно зарегистрировать Expression;
				return new SqlSelectCollection(list.AsReadOnly());
			}
			throw new NotImplementedException();
		}

		private SqlSource VisitSource(Expression exp) {
			var node = VisitInner(exp);
			var source = node as SqlSource;
			if (source != null) {
				RegisterAnonymouseMembers(source);
				return source;
			}
			var query = node as SqlQuerySpecification;
			if (query != null) {
				var subquery=new SqlSubquery(query);
				var selectionList=subquery.Query.Columns;
				_sourceColumns[subquery]=selectionList;
				RegisterAnonymouseMembers(subquery);
				return subquery;
			}
			throw new NotImplementedException();
		}
		private Expression RemoveQuotes(Expression exp) {
			while (exp.NodeType == ExpressionType.Quote) {
				exp = ((UnaryExpression)exp).Operand;
			}
			return exp;
		}
		private LambdaExpression GetLambda(Expression exp) {
			return RemoveQuotes(exp) as LambdaExpression;
		}
		private SqlLogicalExpression VisitLogical(Expression exp){
			var node = VisitInner(exp);
			if (node == null) {
				return null;
			}
			var logical = node as SqlLogicalExpression;
			if (logical != null) {
				return logical;
			}
			throw new NotSupportedException("expression not logical");
		}
		private SqlLogicalExpression VisitBinaryLogical(BinaryExpression exp) {
			var left = VisitLogical(exp.Left);
			var right = VisitLogical(exp.Right);
			if (exp.Method != null) {
				throw new NotImplementedException();
				//return _sql.MethodCall(exp.Type, exp.Method, null, new SqlExpression[] { left, right }, exp) as SqlLogicalExpression;
			}
			throw new NotImplementedException();
			
			switch (exp.NodeType) {
				case ExpressionType.And:
				case ExpressionType.AndAlso:
				case ExpressionType.Or:
				case ExpressionType.OrElse:
					break;
			}
		}
		private SqlComparision VisitComparision(BinaryExpression exp) {
			var left = VisitExpression(exp.Left);
			var right = VisitExpression(exp.Right);
			if (exp.Method != null) {
				throw new NotImplementedException();
				//return _sql.MethodCall(exp.Type, exp.Method, null, new[] { left, right });
			}
			switch (exp.NodeType) {
				case ExpressionType.Equal:
					return new SqlComparision(left, SqlNodeType.EQ, right);
				case ExpressionType.NotEqual:
					return new SqlComparision(left,SqlNodeType.NotEQ, right);
				case ExpressionType.GreaterThan:
					return new SqlComparision(left, SqlNodeType.GT, right);
				case ExpressionType.GreaterThanOrEqual:
					return new SqlComparision(left, SqlNodeType.GE, right);
				case ExpressionType.LessThan:
					return new SqlComparision(left, SqlNodeType.LT, right);
				case ExpressionType.LessThanOrEqual:
					return new SqlComparision(left, SqlNodeType.LE, right);
			}
			throw new NotSupportedException("expression type not supported");
		}


		private SqlExpression VisitBinary(BinaryExpression exp) {
			var left = VisitExpression(exp.Left);
			var right = VisitExpression(exp.Right);
			if (exp.Method != null) {
				//throw new NotImplementedException();
				//return _sql.MethodCall(exp.Type, exp.Method, null, new[] { left, right });
			}
			switch (exp.NodeType) {
				case ExpressionType.Add:
				case ExpressionType.AddChecked:
					return new SqlBinaryScalarExpression(left,SqlNodeType.Add, right);
				case ExpressionType.Coalesce:
					throw new NotImplementedException("coalesce not implemented");
				case ExpressionType.Divide:
					return new SqlBinaryScalarExpression(left, SqlNodeType.Div, right);
				case ExpressionType.ExclusiveOr:
					return new SqlBinaryScalarExpression(left, SqlNodeType.BitXor, right);
				case ExpressionType.Modulo:
					return new SqlBinaryScalarExpression(left, SqlNodeType.Mod, right);
				case ExpressionType.Multiply:
				case ExpressionType.MultiplyChecked:
					return new SqlBinaryScalarExpression(left, SqlNodeType.Mul, right);
				case ExpressionType.Power:
					throw new NotImplementedException("power not supported");
				case ExpressionType.Subtract:
				case ExpressionType.SubtractChecked:
					return new SqlBinaryScalarExpression(left, SqlNodeType.Sub, right);
			}
			throw new NotSupportedException("NodeType not supported");
		}

		private SqlExpression VisitExpression(Expression exp) {
			var node = VisitInner(exp);
			if (node == null) {
				return null;
			}
			var expression = node as SqlExpression;
			if (expression != null) {
				return expression;
			}
			var select = node as SqlQueryExpressionBase;
			if (select == null) {
				throw new InvalidOperationException("unsupported expression");
			}
			throw new NotImplementedException("sub select is not supported yet");
		}
		private void RegisterAnonymouseMembers(SqlSource source) {
			if (_anonymouseTuple != null) {
				var sourceRef = new SelectRef(source);
				for (int i = 0; i < _anonymouseTuple.SelectCollection.SelectList.Count; i++) {
					// TODO - do the same to detect alias (see higher)
					var member = _anonymouseTuple.NewExpression.Members[i];
					var function = member as MethodInfo;
					if (function != null && function.IsSpecialName) {
						string propName = function.Name.Substring(4);
						var property = member.DeclaringType.GetProperty(propName);
						if (property != null && property.GetGetMethod() == function) {
							member = property;
						}
						//member=function.Pr
					}
					_memberExpressions.Add(new MemberSelectionKey(_anonymouseTuple.SelectCollection, member), new SqlSelectItemRef(_anonymouseTuple.SelectCollection.SelectList[i], sourceRef));
				}
				_anonymouseTuple = null;
			}
		}
	}
}
